資料傳遞的方法 props
/ emit
/ v-model
明顯看到主要是以一層傳遞一層的方法執行,但是如果要多層怎麼辦呢?
這時候就要靠 provide
/ inject
依賴注入
provide
/ inject
是什麼(核心觀念)provide(key, value)
註冊一個鍵值。inject(key)
取到同一個 value
(同一個引用/引用型別,不是複製品)。provide()
本身不會讓值變反應式,你提供的值要自己是 ref
或 reactive
,子孫才會跟著更新。inject()
在子孫建立時解析,目前拿到的是祖先「當時」提供的那個引用。之後的變化要靠「引用內的值改變」來傳遞(例如改 ref.value
),而不是再呼叫一次 provide()
。簡圖(方向:資料/服務向下游傳遞):
Root -- provide(key → value/ref/reactive) --> Child --> Grandchild (inject(key))
provide
/ inject
不適合:只是單純的「父傳子」「子回父」日常資料流,這時用
props
/emits
更直覺且可追蹤。
import type { InjectionKey, Ref } from 'vue'
export interface CounterCtx {
count: Ref<number>
inc: () => void
}
export const CounterKey: InjectionKey<CounterCtx> = Symbol('Counter')
<script setup lang="ts">
import { ref, provide, readonly } from 'vue'
import { CounterKey } from './keys'
const count = ref(0)
const inc = () => { count.value++ }
provide(CounterKey, { count: readonly(count), inc }) // 提供「可讀 readonly + 方法」,避免子孫直接改 count
</script>
<template>
<slot />
</template>
<script setup lang="ts">
import { inject } from 'vue'
import { CounterKey } from './keys'
const ctx = inject(CounterKey)
if (!ctx) throw new Error('Counter provider not found')
function onClick() {
ctx.inc()
}
</script>
<template>
<button @click="onClick">+1</button>
<p>count: {{ ctx.count }}</p>
</template>
示意圖:
在表單開發中,常見的需求是讓「表單群組 (FormGroup)」集中管理資料,並讓「表單項目 (FormItem)」能直接讀取或更新這份資料。如果單純依靠 props
和 emit
,每一層元件都需要手動傳遞資料,既繁瑣又容易出錯。
Vue 提供的 provide
/ inject
模式正好能解決這個問題。
FormGroup
作為提供者 (Provider),建立並管理一個共享的 model
,再透過 provide
將它傳遞給子孫元件。
/** FormGroup(Provider)*/
<script setup lang="ts">
import { reactive, provide, readonly, type InjectionKey } from 'vue'
interface FormCtx {
model: Record<string, any>
update: (name: string, val: any) => void
}
// 使用 Symbol 作為 provide/inject 的唯一 key,並且型別化 (InjectionKey<FormCtx>)
export const FormKey: InjectionKey<FormCtx> = Symbol('Form')
const model = reactive({}) // 全組共享
function update(name: string, val: any){ model[name] = val }
// 把 model 和 update 提供給子元件,並透過 readonly(model) → 保證子層不能直接改資料,只能透過 update
provide(FormKey, { model: readonly(model), update })
</script>
<template><slot /></template>
FormItem
作為消費者 (Consumer),無論嵌套在幾層內部,都能透過 inject
直接取得 model
和更新方法。
/** FormItem(Consumer)*/
<script setup lang="ts">
import { inject, toRef, watch } from 'vue'
import { FormKey } from './FormGroup.vue'
const props = defineProps<{ name: string; modelValue?: any }>()
const emit = defineEmits<{ (e:'update:modelValue', v:any): void }>()
// 透過 inject(FormKey) 連接到 FormGroup 提供的 model 與 update
const form = inject(FormKey)
if (!form) throw new Error('FormGroup missing')
watch(() => props.modelValue, v => form.update(props.name, v))
</script>
<template>
<input
:value="form.model[props.name]"
@input="form.update(props.name, $event.target.value)"
/>
</template>
最終,FormGroup 只需要用 <slot>
包裹子元件,整個表單的結構就能保持乾淨,同時所有 FormItem 也能共享並同步這份資料。
<FormGroup>
<FormItem name="username" />
<FormItem name="email" />
</FormGroup>